package com.atguigu.tingshu.common.login.aspect;

import com.alibaba.fastjson.JSONObject;
import com.atguigu.tingshu.common.constant.PublicConstant;
import com.atguigu.tingshu.common.constant.RedisConstant;
import com.atguigu.tingshu.common.execption.GuiguException;
import com.atguigu.tingshu.common.login.annotation.TingshuLogin;
import com.atguigu.tingshu.common.result.ResultCodeEnum;
import com.atguigu.tingshu.common.util.AuthContextHolder;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.security.jwt.Jwt;
import org.springframework.security.jwt.JwtHelper;
import org.springframework.security.jwt.crypto.sign.RsaVerifier;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import java.lang.reflect.Method;

@Aspect
@Component

public class LoginAspect {

    @Autowired
    private StringRedisTemplate redisTemplate;

    /**
     * 定义切面逻辑(判断请求的资源是否登录了 如果登陆了 直接访问这个资源 反之 去登陆)
     * 通知类型 5种
     *  前置通知
     *    特点：切面逻辑在执行目标方法之前执行到
     *    用法：@Before
     *  返回通知：(肯定获取不到目标方法期间出现异常对象 但是可以拿到执行玩目标方法的执行结果)
     *    特点：切面逻辑在执行玩目标方法之后(执行目标方法期间不会出现异常) 来执行到
     *    用法：@AfterReturning
     *  后置通知：(不能捕获到执行期间出现的异常对象)
     *    特点：切面逻辑在执行完目标方法之后(执行目标方法期间不管是否出现异常) 来执行到
     *    用法：@After
     *  异常通知：(可以捕获执行目标方法期间出现的异常对象 肯定获取不到执行目标方法后的结果对象)
     *     特点：切面逻辑在执行玩目标方法之后(执行目标方法期间出现异常) 来执行到
     *     用法：@AfterThrowing
     *  环绕通知：(ProceedingJoinPoint 即能获取到执行目标方法后的结果对象 也能获取到执行目标方法期间的异常对象)
     *     特点：目标方法执行前后以及执行目标方法期间是否出现异常都会执行切面逻辑
     *     用法：@Around
     *
     *
     *
     *     ThreadLocal在使用的过程中应该注意内存泄露的问题
     *     内存泄漏：一个现成的对象（数据）村凡高了内存的堆空间，但是当这个线程干完活之后 没有将堆中刚刚放挤奶器的对象（数据）清空
     *     一次内存泄露可以不用管 但是久而久之 内存泄六就会导致oom (内存溢出)
     *
     *  使用ThreadLocal也不一定出现内存泄漏
     *  1.new Thread
     */

    /*
        多切面的失效问题
            1，登陆切面(业务)
            2，事务切面(非业务)
     */

    @Around(value = "@annotation(com.atguigu.tingshu.common.login.annotation.TingshuLogin)")
    public Object loginCheck(ProceedingJoinPoint pjp) throws Throwable {
        // 前置通知逻辑

        //1.获取请求中的令牌
        String jsonWebToken = getJsonWebToken();

        //2.获取目标方法的注解以及属性值
        Boolean isLogin = getMethodLoginAnnotationValue(pjp);
        if (!isLogin){  //不一定要登陆
            if (StringUtils.isEmpty(jsonWebToken)){
                //如果你要登陆了 即也带了token 那么把用户id传进来
                return pjp.proceed();
            }
        }

        //3.判断是否携带了令牌(如果携带令牌还将载荷中的数据捕获到)
        Long userId = checkTokenAndGetCliams(jsonWebToken);

        //4.将认证中获取到的userId放到ThreadLocal中
        AuthContextHolder.setUserId(userId);

//        ThreadLocal<Long> threadLocal = new ThreadLocal<>();
//        threadLocal.set(userId);

        Object retVal;
        try{
            retVal = pjp.proceed();//执行目标方法
        }finally {
            AuthContextHolder.removeUserId(); //解决内存泄漏
        }
        // 5.返回通知
        return retVal;
    }

    private Boolean getMethodLoginAnnotationValue(ProceedingJoinPoint pjp) {
        //1.获取方法的签名对象
        MethodSignature methodSignature = (MethodSignature) pjp.getSignature();

        //2.获取方法对象
        Method method = methodSignature.getMethod();

        //3.获取方法上的TingshuLogin注解对象
        TingshuLogin annotation = method.getAnnotation(TingshuLogin.class);

        //4.获取注解对象的属性值
        boolean required = annotation.required();
        //5.返回
        return required;
    }

    private Long checkTokenAndGetCliams(String jsonWebTokenFromWeb) {
        //1.校验是否改请求中携带了令牌
        if (StringUtils.isEmpty(jsonWebTokenFromWeb)){
            throw new GuiguException(ResultCodeEnum.LOGIN_AUTH);
        }
        //2.校验jsonWebToken是否被篡改了
        Jwt jwt = JwtHelper.decodeAndVerify(jsonWebTokenFromWeb, new RsaVerifier(PublicConstant.PUBLIC_KEY));
        //3.校验通过，获取载荷数据
        String claims = jwt.getClaims();

        JSONObject map = JSONObject.parseObject(claims);
        Object userId = map.get("id");
        Object openId = map.get("openId");

        //4.对比Redis种是否存在jsonWebToken
        String accessTokenKey = RedisConstant.USER_LOGIN_KEY_PREFIX + openId;
        String accessTokenKeyFromRedis = redisTemplate.opsForValue().get(accessTokenKey);
        if (StringUtils.isEmpty(accessTokenKeyFromRedis) || !jsonWebTokenFromWeb.equals(accessTokenKeyFromRedis)){
            throw new GuiguException(401,"accessToken已过期");
        }

        return Long.parseLong(userId.toString());
    }

    private static String getJsonWebToken() {
        //1.获取用户的身份信息
        //1.1获取目标请求属性对象
        ServletRequestAttributes requestAttributes =(ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        //1.2获取请求对象
        HttpServletRequest request = requestAttributes.getRequest();
        //1.3获取请求对象的请求头
        String token = request.getHeader("token");
        return token;
    }


}
